// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Defines functionality for browser tabs.  Also provides
 * signals to notify users of when certain actions occur.
 *
 * @author jasonstredwick@google.com (Jason Stredwick)
 */


goog.provide('mimic.TabManager');

goog.require('bite.common.signal.Simple');



/**
 * A class that manages "windows"; i.e. tabs.
 * @constructor
 */
mimic.TabManager = function() {
  /**
   * Provide a signal for when an update has occurred for a tab with the
   * following information: {location: url, window: window object}.
   * @type {!bite.common.signal.Simple}
   */
  this.onUpdate = new bite.common.signal.Simple();

  /**
   * A number used to generate unique ids.
   * @type {number}
   * @private
   */
  this.uniqueId_ = 1;

  /**
   * A list of "windows" (i.e. tabs) within a ChromeWindow and their
   * associated unique id.
   * @type {!Array.<!Object.<string, !Object>>}
   * @private
   */
  this.windowInfoList_ = [];

  /**
   * An object that manages the various observer objects.
   * @type {!Object}
   * @private
   */
  this.Observer_ = {
    contentCreated: {'observe': goog.bind(this.handleContentCreated_, this)},
    contentDestroyed:
        {'observe': goog.bind(this.handleContentDestroyed_, this)}
  };

  // Add Observers.
  var component = Components.classes['@mozilla.org/observer-service;1'];
  var Ci = Components.interfaces;
  var observers = component.getService(Ci.nsIObserverService);
  observers.addObserver(this.Observer_.contentCreated,
                        'content-document-global-created', false);
  observers.addObserver(this.Observer_.contentDestroyed,
                        'dom-window-destroyed', false);
};
goog.addSingletonGetter(mimic.TabManager);


/**
 * "Destroy" a tab manager by setting its data back to initial values and
 * remove observers.
 */
mimic.TabManager.prototype.destroy = function() {
  this.onUpdate.clear();

  var component = Components.classes['@mozilla.org/observer-service;1'];
  var Ci = Components.interfaces;
  var observers = component.getService(Ci.nsIObserverService);
  observers.removeObserver(this.Observer_.contentCreated,
                           'content-document-global-created');
  observers.removeObserver(this.Observer_.contentDestroyed,
                           'dom-window-destroyed');

  this.uniqueId_ = 1;
  this.windowInfoList_ = [];
};


/**
 * Retrieves the tab object's related information or null if not present.
 * @param {!XPCNativeWrapper} window The window object for the newly created
 *     content.
 * @return {Object} The window's related information.
 * @private
 */
mimic.TabManager.prototype.getInfo_ = function(window) {
  for (var i = 0; i < this.windowInfoList_.length; ++i) {
    var info = this.windowInfoList_[i];
    if (info.ref == window) {
      return info;
    }
  }

  return null;
};


/**
 * Generates a unique number to be used as a Window id.
 * @return {number} A unique id.
 * @private
 */
mimic.TabManager.prototype.getUniqueId_ = function() {
  var id = this.uniqueId_;
  ++this.uniqueId_;
  return id;
};


/**
 * Handle the 'content-document-global-created' event processed by the
 * ObserverService.
 * @param {!XPCNativeWrapper} window The window object created for the new
 *     content; i.e. a tab's window object.
 * @param {string} topic The event type.
 * @param {?string} data The data will be the url (origin) for the new content.
 *     Or null if the content has no origin, these will be ignored.
 * @private
 */
mimic.TabManager.prototype.handleContentCreated_ = function(window, topic,
                                                            data) {
  // TODO (jasonstredwick): Figure out appropriate method of identifying which
  // Windows should be managed; checking data (i.e. the url) is not appropriate
  // as some Windows to be managed have no data given at the time this event
  // is envoked.
  if (!window.wrappedJSObject) {
    return;
  }

  var info = this.getInfo_(window);
  if (!info) {
    var id = this.getUniqueId_();
    var wrapper = window;

    info = {
      id: id,
      loaded: false,
      fireOnUpdateWrapper: null,
      ref: window,
      wrapper: wrapper
    };

    this.windowInfoList_.push(info);
  }

  // TODO (jasonstredwick): Determine if it is necessary to assign a listener
  // to the onload event if the url changes or if the listener remains after
  // a url change occurs.
  // TODO (jasonstredwick): Figure out why this throws an exception.
  //if (info.fireOnUpdateWrapper) {
  //  info.wrapper.removeEventListener('load', info.fireOnUpdateWrapper, true);
  //}

  info.fireOnUpdateWrapper =
      goog.partial(function(signal, location, id, wrapper) {
        signal.fire({location: location, id: id, window: wrapper});
      }, this.onUpdate, info.wrapper.location, info.id, info.wrapper);

  info.wrapper.addEventListener('load', info.fireOnUpdateWrapper, true);
};


/**
 * Handle the 'dom-window-destroyed' event processed by the ObserverService.
 * TODO (jasonstredwick): Determine if data is always null.
 * @param {!XPCNativeWrapper} window The window object created for the new
 *     content; i.e. a tab's window object.
 * @param {string} topic The event type.
 * @param {?string} data Always null.
 * @private
 */
mimic.TabManager.prototype.handleContentDestroyed_ = function(window, topic,
                                                              data) {
  for (var i = 0; i < this.windowInfoList_.length; ++i) {
    var info = this.windowInfoList_[i];
    if (info.ref == window) {
      // TODO (jasonstredwick): Figure out why this throws an exception.
      //info.wrapper.removeEventListener('load', info.fireOnUpdateWrapper,
      //                                 true);

      // Remove the info from the list of tabs.
      this.windowInfoList_.splice(i, 1);
      break;
    }
  }
};

